package com.xuecheng.media.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import com.xuecheng.base.exception.XueChengPlusException;
import com.xuecheng.base.model.PageParams;
import com.xuecheng.base.model.PageResult;
import com.xuecheng.base.model.RestResponse;
import com.xuecheng.media.mapper.MediaFilesMapper;
import com.xuecheng.media.model.dto.QueryMediaParamsDto;
import com.xuecheng.media.model.dto.UploadFileParamsDto;
import com.xuecheng.media.model.po.MediaFiles;
import com.xuecheng.media.service.MediaFileService;
import io.minio.*;
import io.minio.errors.*;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.*;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 媒资文件管理接口实现类
 */
@Slf4j
@Service
public class MediaFileServiceImpl implements MediaFileService {

    @Autowired
    MediaFilesMapper mediaFilesMapper;

    @Autowired
    private MinioClient minioClient;

    /**
     * 问题：非事务方法调用事务方法，事务失效
     * 解决：将自己注入进来变成代理对象，用代理对象去调用事务方法
     */
    @Autowired
    private MediaFileServiceImpl currentProxy;

    // 普通文件桶
    @Value("${minio.bucket.files}")
    private String bucket_files;

    // 视频桶
    @Value("${minio.bucket.videofiles}")
    private String bucket_videos;

    /**
     * 查询媒资信息
     *
     * @param companyId           机构id
     * @param pageParams          分页参数
     * @param queryMediaParamsDto 查询条件
     * @return 分页之后的媒资信息
     */
    @Override
    public PageResult<MediaFiles> queryMediaFiels(Long companyId, PageParams pageParams, QueryMediaParamsDto queryMediaParamsDto) {
        String fileType = queryMediaParamsDto.getFileType();
        String auditStatus = queryMediaParamsDto.getAuditStatus();
        String filename = queryMediaParamsDto.getFilename();

        // 构建查询条件对象
        LambdaQueryWrapper<MediaFiles> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(MediaFiles::getCompanyId, companyId)
                .eq(StringUtils.isNotEmpty(fileType), MediaFiles::getFileType, fileType)
                .eq(StringUtils.isNotEmpty(auditStatus), MediaFiles::getAuditStatus, auditStatus)
                .like(StringUtils.isNotEmpty(filename), MediaFiles::getFilename, filename);

        // 判断是否是课程计划关联媒资操作
        if (pageParams.getPageNo() == null || pageParams.getPageSize() == null) {
            // 查询数据内容获得结果
            List<MediaFiles> mediaFiles = mediaFilesMapper.selectList(queryWrapper);
            // 构造返回对象
            PageResult<MediaFiles> result = new PageResult<>();
            result.setItems(mediaFiles);
            // 返回结果
            return result;
        }

        // 分页对象
        Page<MediaFiles> page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize());
        // 查询数据内容获得结果
        Page<MediaFiles> pageResult = mediaFilesMapper.selectPage(page, queryWrapper);
        // 获取数据列表
        List<MediaFiles> list = pageResult.getRecords();
        // 获取数据总数
        long total = pageResult.getTotal();
        // 构建结果集
        PageResult<MediaFiles> mediaListResult = new PageResult<>(list, total, pageParams.getPageNo(), pageParams.getPageSize());
        return mediaListResult;
    }

    /**
     * 上传文件到Minio
     *
     * @param bucket        桶（文件夹）
     * @param objectName    对象名（文件名）
     * @param localFilePath 存储的本地路径
     * @param mimeType      文件类型
     */
    public void addMediaFilesToMinIO(String bucket, String objectName, String localFilePath, String mimeType) {
        try {
            // 上传文件所需参数
            UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
                    .bucket(bucket)
                    .object(objectName)
                    .filename(localFilePath)
                    .contentType(mimeType)
                    .build();

            // 执行上传文件
            minioClient.uploadObject(uploadObjectArgs);
            log.debug("上传文件到Minio成功,bucket:{},objectName:{}", bucket, objectName);
        } catch (Exception e) {
            log.error("上传文件到Minio出错,bucket:{},objectName:{},错误原因:{}", bucket, objectName, e.getMessage(), e);
            XueChengPlusException.cast("上传文件到文件系统失败");
        }
    }

    /**
     * 上传文件到Minio
     *
     * @param bytes      文件字节数组
     * @param bucket     桶（文件夹）
     * @param objectName 对象名（文件名）
     */
    private void addMediaFilesToMinIO(byte[] bytes, String bucket, String objectName) {
        // 字节数组输入流
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
        // 默认content-type为未知二进制流
        String contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;

        // 判断对象名是否包含 .
        if (objectName.contains(".")) {
            // 截出后缀
            String extension = objectName.substring(objectName.lastIndexOf("."));
            // 获取对应的类型
            contentType = getMimeType(extension);
        }
        try {
            minioClient.putObject(PutObjectArgs.builder()
                    .bucket(bucket)
                    .object(objectName)
                    .stream(byteArrayInputStream, byteArrayInputStream.available(), -1)
                    .contentType(contentType)
                    .build());
        } catch (Exception e) {
            log.debug("上传到文件系统出错:{}", e.getMessage());
            throw new XueChengPlusException("上传到文件系统出错");
        }
    }

    /**
     * 根据文件拓展名获取mimeType
     *
     * @param extension 文件拓展名
     * @return mimeType
     */
    private String getMimeType(String extension) {
        // 判断拓展名是否为空
        if (extension == null) extension = "";
        // 根据扩展名取出mimeType
        ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
        // 默认content-type为未知二进制流
        String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
        // 如果得到了正常的content-type，则重新赋值，覆盖默认类型
        if (extensionMatch != null) {
            mimeType = extensionMatch.getMimeType();
        }
        return mimeType;
    }

    /**
     * 获取文件的MD5
     *
     * @param file 文件
     * @return 文件的md5摘要
     */
    private String getFileMd5(File file) {
        try (FileInputStream fileInputStream = new FileInputStream(file)) {
            return DigestUtils.md5Hex(fileInputStream);
        } catch (Exception e) {
            log.error("获取文件MD5摘要失败：{}", e.getMessage());
            return null;
        }
    }

    /**
     * 获取文件默认存储目录路径
     *
     * @return 年/月/日/
     */
    private String getDefaultFolderPath() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        return sdf.format(new Date()).replace("-", "/") + "/";
    }

    /**
     * 添加媒资信息到数据库
     *
     * @param fileMD5    文件MD5值（用作主键id）
     * @param companyId  机构名称
     * @param dto        媒资信息
     * @param bucket     桶（文件夹）
     * @param objectName 对象名称（文件名称）
     * @return MediaFiles
     */
    @Transactional
    public MediaFiles addMediaFilesToMinDB(String fileMD5, Long companyId, UploadFileParamsDto dto, String bucket, String objectName) {
        // 查询该文件是否已存在
        MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMD5);
        if (mediaFiles == null) {
            // 创建对象
            mediaFiles = new MediaFiles();
            // 拷贝基础信息
            BeanUtils.copyProperties(dto, mediaFiles);
            // 设置主键id
            mediaFiles.setId(fileMD5);
            // 设置机构id
            mediaFiles.setCompanyId(companyId);
            // 设置文件id
            mediaFiles.setFileId(fileMD5);
            // 设置文件访问路径
            mediaFiles.setUrl("/" + bucket + "/" + objectName);
            // 设置桶
            mediaFiles.setBucket(bucket);
            // 设置文件路径（包含目录）
            mediaFiles.setFilePath(objectName);

            // 插入数据库
            int inserted = mediaFilesMapper.insert(mediaFiles);
            // 判断插入是否成功
            if (inserted != 1) XueChengPlusException.cast("保存文件信息失败");
        }
        return mediaFiles;
    }

    /**
     * 上传文件
     *
     * @param companyId     机构id
     * @param dto           上传文件信息
     * @param localFilePath 本地文件磁盘路径
     * @return MediaFiles
     */
    @Override
    public MediaFiles uploadFile(Long companyId, UploadFileParamsDto dto, String localFilePath) {
        // 根据文件本地路径获取文件（本地磁盘暂存）
        File file = new File(localFilePath);
        // 判断文件是否存在
        if (!file.exists()) XueChengPlusException.cast("文件不存在");
        // 获取文件名称
        String filename = dto.getFilename();
        // 根据文件名称，获取文件拓展名
        String extension = filename.substring(filename.lastIndexOf("."));
        // 根据文件拓展名，获取mimeType
        String mimeType = getMimeType(extension);
        // 获取文件MD5摘要，用作文件名称
        String fileMd5 = getFileMd5(file);
        // 获取文件的默认目录（用于存储在Minio上面的目录）
        String defaultFolderPath = getDefaultFolderPath();
        // 拼接对象名称（存储到Minio中的对象名(带目录)）
        String objectName = defaultFolderPath + fileMd5 + extension;

        // 将文件上传到Minio
        addMediaFilesToMinIO(bucket_files, objectName, localFilePath, mimeType);

        // 获取文件大小，并设置到dto中
        dto.setFileSize(file.length());

        // 将文件保存到数据库
        MediaFiles mediaFiles = currentProxy.addMediaFilesToMinDB(fileMd5, companyId, dto, bucket_files, objectName);

        // 返回数据
        return mediaFiles;
    }

    /**
     * 获取分块文件的目录
     *
     * @param fileMd5 文件MD5
     * @return 块文件的目录
     */
    private String getChunkFileFolderPath(String fileMd5) {
        return fileMd5.charAt(0) + "/" + fileMd5.charAt(1) + "/" + fileMd5 + "/" + "chunk" + "/";
    }

    /**
     * 检查文件是否存在
     *
     * @param fileMd5 文件的md5
     * @return false不存在，true存在
     */
    @Override
    public RestResponse<Boolean> checkFile(String fileMd5) {
        MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
        // 数据库中不存在，则直接返回false 表示不存在
        if (mediaFiles == null) {
            return RestResponse.success(false);
        }
        // 若数据库中存在，根据数据库中的文件信息，则继续判断bucket中是否存在
        try {
            InputStream inputStream = minioClient.getObject(GetObjectArgs
                    .builder()
                    .bucket(mediaFiles.getBucket())
                    .object(mediaFiles.getFilePath())
                    .build());
            if (inputStream == null) {
                return RestResponse.success(false);
            }
        } catch (Exception e) {
            return RestResponse.success(false);
        }
        return RestResponse.success(true);
    }

    /**
     * 检查分块是否存在
     *
     * @param fileMd5    文件的md5
     * @param chunkIndex 分块序号
     * @return false不存在，true存在
     */
    @Override
    public RestResponse<Boolean> checkChunk(String fileMd5, int chunkIndex) {
        // 获取分块目录
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        String chunkFilePath = chunkFileFolderPath + chunkIndex;
        try {
            // 判断分块是否存在
            InputStream inputStream = minioClient.getObject(GetObjectArgs
                    .builder()
                    .bucket(bucket_videos)
                    .object(chunkFilePath)
                    .build());
            // 不存在返回false
            if (inputStream == null) {
                return RestResponse.success(false);
            }
        } catch (Exception e) {
            // 出异常也返回false
            return RestResponse.success(false);
        }
        // 否则返回true
        return RestResponse.success(true);
    }

    /**
     * 上传分块
     *
     * @param fileMd5 文件md5
     * @param chunk   分块序号
     * @param bytes   文件字节
     * @return RestResponse
     */
    @Override
    public RestResponse uploadChunk(String fileMd5, int chunk, byte[] bytes) {
        // 根据文件MD5查询分块文件的目录
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        // 具体的分块
        String chunkFilePath = chunkFileFolderPath + chunk;

        try {
            // 将文件存储至minIO
            addMediaFilesToMinIO(bytes, bucket_videos, chunkFilePath);
            return RestResponse.success(true);
        } catch (Exception ex) {
            ex.printStackTrace();
            log.debug("上传分块文件:{},失败:{}", chunkFilePath, ex.getMessage());
        }
        return RestResponse.validfail(false, "上传分块失败");
    }

    /**
     * 得到合并后的文件的地址
     *
     * @param fileMd5 文件id 即md5值
     * @param fileExt 文件扩展名
     * @return 合并后的文件的地址
     */
    private String getFilePathByMd5(String fileMd5, String fileExt) {
        return fileMd5.charAt(0) + "/" + fileMd5.charAt(1) + "/" + fileMd5 + "/" + fileMd5 + fileExt;
    }

    /**
     * 从minio下载文件
     *
     * @param bucket     桶
     * @param objectName 对象名称
     * @return 下载后的文件
     */
    public File downloadFileFromMinIO(String bucket, String objectName) {
        // 临时文件
        File minioFile = null;
        // 文件输出流
        FileOutputStream outputStream = null;
        // 从minio下载文件
        try {
            InputStream stream = minioClient.getObject(GetObjectArgs.builder()
                    .bucket(bucket)
                    .object(objectName)
                    .build());
            // 创建临时文件
            minioFile = File.createTempFile("minio", ".merge");
            outputStream = new FileOutputStream(minioFile);
            IOUtils.copy(stream, outputStream);
            return minioFile;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (outputStream != null) {
                try {
                    // 关闭文件输出流
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

    /**
     * 清除分块文件
     *
     * @param chunkFileFolderPath 分块文件路径
     * @param chunkTotal          分块文件总数
     */
    private void clearChunkFiles(String chunkFileFolderPath, int chunkTotal) {
        // 合并分块的路径
        try {
            List<DeleteObject> deleteObjects = Stream.iterate(0, i -> ++i)
                    .limit(chunkTotal)
                    .map(i -> new DeleteObject(chunkFileFolderPath.concat(Integer.toString(i))))
                    .collect(Collectors.toList());

            // 移除分块参数
            RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs
                    .builder()
                    .bucket("video")
                    .objects(deleteObjects)
                    .build();
            // 移除分块
            Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs);
            results.forEach(r -> {
                DeleteError deleteError = null;
                try {
                    deleteError = r.get();
                } catch (Exception e) {
                    e.printStackTrace();
                    log.error("清楚分块文件失败,objectname:{}", deleteError.objectName(), e);
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
            log.error("清楚分块文件失败,chunkFileFolderPath:{}", chunkFileFolderPath, e);
        }
    }

    /**
     * 合并分块
     *
     * @param companyId           机构id
     * @param fileMd5             文件md5
     * @param chunkTotal          分块总数
     * @param uploadFileParamsDto 文件信息
     * @return RestResponse
     */
    @Override
    public RestResponse mergechunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto) {
        // 获取分块文件路径
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);

        // 将分块文件路径组成 List<ComposeSource>
        List<ComposeSource> sourceObjectList = Stream.iterate(0, i -> ++i)
                .limit(chunkTotal)
                .map(i -> ComposeSource.builder()
                        .bucket(bucket_videos)
                        .object(chunkFileFolderPath.concat(Integer.toString(i)))
                        .build())
                .collect(Collectors.toList());

        // 获取原文件名称
        String fileName = uploadFileParamsDto.getFilename();
        // 获取文件拓展名
        String extName = fileName.substring(fileName.lastIndexOf("."));
        // 获取合并的文件路径
        String mergeFilePath = getFilePathByMd5(fileMd5, extName);

        // 合并分块
        try {
            minioClient.composeObject(
                    ComposeObjectArgs.builder()
                            .bucket(bucket_videos)
                            .object(mergeFilePath)
                            .sources(sourceObjectList)
                            .build()
            );
        } catch (Exception e) {
            log.debug("合并文件失败,fileMd5:{},异常:{}", fileMd5, e.getMessage(), e);
            return RestResponse.validfail(false, "合并文件失败");
        }

        // 下载刚刚合并完成的视频
        File minioFile = downloadFileFromMinIO(bucket_videos, mergeFilePath);
        // 判断是否下载成功
        if (minioFile == null) {
            log.debug("下载合并后文件失败,mergeFilePath:{}", mergeFilePath);
            return RestResponse.validfail(false, "下载合并后文件失败。");
        }

        // 校验是否合并成功，验证上传前后的MD5值
        try (InputStream fileInputStream = new FileInputStream(minioFile)) {
            // minio上文件的md5值
            String md5FormMinio = DigestUtils.md5Hex(fileInputStream);
            // 比较md5值，不一致则说明文件不完整
            if (!fileMd5.equals(md5FormMinio)) {
                return RestResponse.validfail(false, "文件合并校验失败，上传失败");
            }
            // 文件大小
            uploadFileParamsDto.setFileSize(minioFile.length());
        } catch (Exception e) {
            log.debug("校验文件失败,fileMd5:{},异常:{}", fileMd5, e.getMessage(), e);
            return RestResponse.validfail(false, "文件合并校验失败，上传失败。");
        } finally {
            minioFile.delete();
        }

        // 文件入库
        currentProxy.addMediaFilesToMinDB(fileMd5, companyId, uploadFileParamsDto, bucket_videos, mergeFilePath);

        // 清除分块文件
        clearChunkFiles(chunkFileFolderPath, chunkTotal);
        // 返回结果
        return RestResponse.success(true);
    }

    /**
     * 根据媒资id获取媒资文件
     *
     * @param mediaId 媒资id
     * @return 媒资文件
     */
    @Override
    public MediaFiles getFileById(String mediaId) {
        if (mediaId == null) XueChengPlusException.cast("媒资id为空");
        return mediaFilesMapper.selectById(mediaId);
    }

}
